package com.timber.util;


import javax.swing.*;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;


/**
 * SearchableComboBox uses vector of objects that extend SLDataObjectAdaptor or implement SLDataObject.
 *
 * <pre>
 * listString() method of SLDataObject should return unique short name
 * getDescr() method of SLDataObject should return long name
 * toString() method of SLDataObject should return short name +  long name
 * isPlaceHolderObject() method of SLDataObject should return false
 * SearchableComboBox supports all main methods of javax.swing.JComboBox:
 * getSelectedItem(), setSelectedIndex(int), setEditable(boolean), etc.
 * CONSTRUCTOR PARAMTERS:
 * parent              - JFrame or JDialog where SearchableComboBox is instantiated;
 * v                   - Vector of SLDataObjects to be displayed in the combo;
 * defaultItem         - int, defines MASK for additional item(s) in the combo:
 * SearchableComboBox.INCLUDE_ALL inserts "--  ALL  --"
 * SearchableComboBox.INCLUDE_NONE inserts "--  NONE  --"
 * SearchableComboBox.INCLUDE_NOTHING  does not insert any additional items
 *  multiSelect         - if true, selection of more than 1 item is allowed
 *  example: SearchableComboBox customerCoB
 *       = new SearchableComboBox(this, CustomerMgr.mgr.getDataVector(), SearchableComboBox.INCLUDE_ALL, true);
 *  By default, a combobox allows selection of multiple items, neither "ALL" or "NONE" option is included
 *  example: SearchableComboBox customerCoB = new SearchableComboBox(this, CustomerMgr.mgr.getDataVector());
 * Additional features:
 * example: searchableComboBox.select_once = false;
 * Setting boolean select_once to false  allows selecting THE SAME item multiple times, for example,
 * for building search string "*.*"
 * example: setDisplayStr2Descr(true);
 * Calling this method will result in displaying object's description in the combo instead of a short name
 * example: setValidate(true);
 * Calling this method will prevent user from typing invalid string into combo box
 *
 * example: hideDetailButton(); - hides detail button
 * Typed / Selected items are always in UPPER CASE
 *</pre>
 */

public class SearchableComboBox extends JPanel {
    /**
     * Implemennt this interface if you want to filter items of the combo
     */
    public interface Filter {
        /**
         * This method should return true if the item is valid
         * for including to the combo.
         */
        public boolean isValidItem(Object item);
    }

    /**		no additional items 	*/
    public static final int INCLUDE_NOTHING = 0;
    /**		add ' -- ALL --' to list	*/
    public static final int INCLUDE_ALL = 1;
    /**		add ' -- NONE --' to list	*/
    public static final int INCLUDE_NONE = 2;
    /**		add '' to list	*/
    public static final int INCLUDE_EMPTY = 4;

    public static final String SELECT_NONE = SelectNone.SELECT_NONE;
    public static final String SELECT_ALL = SelectAll.SELECT_ALL;
    public static final String SELECT_EMPTY = SelectEmpty.SELECT_EMPTY;

    protected SLDataObject defaultObj = null;
    protected boolean displayDescr = false;
    public boolean select_once = true; //prevents selecting the same item twice in the lookup window (however, it is needed for Melissa's screen)
    protected boolean multi_select = true;
    protected boolean validate = true;
    protected int defaultItem = INCLUDE_NOTHING;
    protected UpperCaseComboBox itemsCoB = new UpperCaseComboBox();
    protected FlowLayout flowLayout3 = new FlowLayout();
    protected GridBagLayout gridBagLayout1 = new GridBagLayout();
    protected JButton btnDetail = new JButton();
    protected TitledBorder titledBorder1;
    protected Vector items;        // Vector of SLDataObject
    protected Vector dataVector;

    public SearchableDialog dialog;		// pop-up selection dialog

    protected SearchableComboBoxEditor editor = null;

    protected Color focusBG;
    protected Color nonFocusBG;
    private boolean showDetailButton = true;

    private int x_coord = 2;
    private Filter filter = null;


    // no args constructor so that SearchableComboBox maybe extended
    public SearchableComboBox() {
        showDetailButton = true;
    }

    protected SearchableComboBoxEditor getEditor(){
        if( editor == null ){
            editor = new SearchableComboBoxEditor();
        }
        return editor;
    }

    /**
     *	constructor
     *
     *	@param arr	array of objects. list is build by calling toString()
     */
    public SearchableComboBox(Object[] arr) {
        showDetailButton = false;

        Vector v = new Vector();
        for (int i = 0; i < arr.length; i++)
            v.add(new SLString(arr[i].toString()));

        doInit(v, INCLUDE_NOTHING);
    }

    public SearchableComboBox(JFrame parent, Vector v) {
        this(parent, v, INCLUDE_NOTHING, true);
    }

    public SearchableComboBox(JFrame parent, Vector v, int dfltItem, boolean multiSelect) {
        multi_select = multiSelect;
        dialog = new SearchableComboDialog(parent, this, multiSelect);
        showDetailButton = true;
        doInit(v, dfltItem);
    }

    public SearchableComboBox(JDialog parent, Vector v) {
        this(parent, v, INCLUDE_NOTHING, true);
    }

    public SearchableComboBox(JDialog parent, Vector v, int dfltItem, boolean multiSelect) {
        multi_select = multiSelect;
        showDetailButton = true;
        dialog = new SearchableComboDialog(parent, this, multiSelect);
        doInit(v, dfltItem);
    }

    /**
     *	set additional items mask and reload comboBox
     */
    public void setAdditionalItems(int itemMask) {
        if (defaultItem == itemMask)
            return;

        setDataVector(dataVector, itemMask);
    }

    /**********************************************************************/

    protected void jbInit() throws Exception {
        titledBorder1 = new TitledBorder(BorderFactory.createEmptyBorder(), "Item");
        this.setLayout(gridBagLayout1);
        this.setPreferredSize(new Dimension(130, 60));

        itemsCoB.setEditor(getEditor());
        //itemsCoB.setFont(new java.awt.Font("DialogInput", 1, 11));
        itemsCoB.setFont(((javax.swing.plaf.metal.MetalLookAndFeel) UIManager.getLookAndFeel()).getUserTextFont());
        itemsCoB.setMinimumSize(new Dimension(126, 20));
        itemsCoB.setPreferredSize(new Dimension(230, 20));
        itemsCoB.setEditable(true);

        itemsCoB.getEditor().getEditorComponent().addFocusListener(new java.awt.event.FocusAdapter() {
            public void focusGained(FocusEvent e) {
                itemsCoB_focusGained(e);
            }

            public void focusLost(FocusEvent e) {
                itemsCoB_focusLost(e);
            }
        });

        itemsCoB.getEditor().getEditorComponent().addKeyListener(new SearchableKeyListener());

        nonFocusBG = itemsCoB.getEditor().getEditorComponent().getBackground();

        flowLayout3.setHgap(0);
        flowLayout3.setVgap(0);
        //if( dialog != null )
        if (showDetailButton) {
            btnDetail.setMaximumSize(new Dimension(20, 20));
            btnDetail.setMinimumSize(new Dimension(20, 20));
            btnDetail.setPreferredSize(new Dimension(20, 20));
            btnDetail.setText("...");
            btnDetail.setFocusable(false);

            btnDetail.addActionListener(new java.awt.event.ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    btnDetail_actionPerformed(e);
                }
            });

            this.add(btnDetail, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0
                    , GridBagConstraints.EAST,
                    GridBagConstraints.NONE,
                    new Insets(0, 0, 0, 0), 0, 0));
        }

        this.add(itemsCoB, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0
                , GridBagConstraints.WEST,
                GridBagConstraints.HORIZONTAL,
                new Insets(0, 0, 0, 0), 0, 0));

    }

    /**
     *	Searches for string in the list that begins with values of parameter
     *  and is not contained in excludedValues
     *
     *	@param searchString	upper case string to search for
     *	@param excludedValues	values to be avoided, optional
     */
    protected SLDataObject search(String searchString, List excludedValues) {
        for (Iterator i = items.iterator(); i.hasNext();) {
            SLDataObject item = (SLDataObject) i.next();
            String value = displayDescr ? item.getDescr() : item.listString();
            if (value == null) {
                return null;
            }
            if (select_once && excludedValues != null && excludedValues.contains(value.toUpperCase())) {
                continue;
            }
            if (value.toUpperCase().startsWith(searchString)) {
                return item;
            }
        }
        return null;
    }

    public Object getSelectedItem() {
        return itemsCoB.getEditor().getItem();
    }

    public Object getSelectedItemObj() {
        return itemsCoB.getSelectedItem();
    }

    public int getSelectedIndex() {
        return itemsCoB.getSelectedIndex();
    }

    public void setSelectedItem(Object obj) {
        itemsCoB.setSelectedItem(obj);
    }

    public void setSelectedIndex(int indx) {
        itemsCoB.setSelectedIndex(indx);
    }

    public void addItemListener(ItemListener l) {
        itemsCoB.addItemListener(l);
    }

    public void addActionListener(ActionListener l) {
        itemsCoB.addActionListener(l);
    }

    public void removeActionListener(ActionListener l) {
        itemsCoB.removeActionListener(l);
    }

    public void setSelectedItems(java.util.List items) {
        ListIterator li = items.listIterator();
        StringBuffer sb = new StringBuffer();

        while (li.hasNext()) {
            Object obj = li.next();
            if (sb.length() > 0) sb.append(",");
            sb.append(obj.toString());
        }
        ((UpperCaseField) itemsCoB.getEditor().getEditorComponent()).setText(sb.toString());
    }


    public void setEditable(boolean flag) {
        UpperCaseField editor = (UpperCaseField) itemsCoB.getEditor().getEditorComponent();
        editor.setEditable(flag);
    }

    public void setEnabled(boolean flag) {
        UpperCaseField editor = (UpperCaseField) itemsCoB.getEditor().getEditorComponent();
        editor.setEnabled(flag);
        itemsCoB.setEnabled(flag);
        btnDetail.setEnabled(flag);
    }

    /**
     *	elipses btn clicked, bring up selection dialog window
     *
     */
    protected void btnDetail_actionPerformed(ActionEvent e) {
        Vector selectedItems = new Vector();
        for (Iterator i = getTypedValues().iterator(); i.hasNext();) {
            String value = (String) i.next();
            for (Iterator j = items.iterator(); j.hasNext();) {
                SLDataObject item = (SLDataObject) j.next();
                if (item.isPlaceHolderObject()) {
                    continue;
                }
                String holder = displayDescr ? item.getDescr() : item.listString();
                if (holder != null && value.equalsIgnoreCase(holder)) {
                    selectedItems.add(item);
                    break;
                }
            }
        }

        if (getContainedObjectsClass() != null) {
            DefaultComboBoxModel mdl = new DefaultComboBoxModel(SearchableItem.getMostSearchedSelection(getContainedObjectsClass()));
            mdl.insertElementAt("Pre-saved Selections", 0);
            dialog.selectionCoB.setModel(mdl);
            dialog.selectionCoB.setSelectedIndex(0);
        }
        dialog.selectedTm.setTableData(selectedItems);
        dialog.setModal(true);
        dialog.showDialog();
    }

    // highlight selected item when tabbed in
    protected void itemsCoB_focusGained(FocusEvent e) {
        if (itemsCoB.getEditor().getItem() != null) {
            String s = itemsCoB.getEditor().getItem().toString();
            JTextField tf = (JTextField) itemsCoB.getEditor().getEditorComponent();
            tf.getCaret().setDot(0);
            tf.getCaret().moveDot(s.length());
        }
        if (focusBG != null)
            itemsCoB.getEditor().getEditorComponent().setBackground(focusBG);
    }

    //select first element when tabbed out
    protected void itemsCoB_focusLost(FocusEvent e) {
        if (itemsCoB.getEditor().getItem() == null) {
            //  Call this to make sure we get the first item's string
            itemsCoB.getEditor().setItem(itemsCoB.getItemAt(0));

            //  Call this to fire off any item listeners
            setSelectedIndex(0);
        }
        itemsCoB.getEditor().getEditorComponent().setBackground(nonFocusBG);
    }

    /** @deprecated **/
    public void setDataVector(Vector v, boolean includeAll) {
        items = (Vector) v.clone();
        if (includeAll && (items.size() < 1 || (defaultObj == null || !items.elementAt(0).toString().equals(defaultObj.listString()))))
            items.insertElementAt(new SelectAll(), 0);
        refresh();
    }

    /**
     *	set contents of the combo box to Vector with NONE as default
     */
    public void setDataVector(Vector v) {
        setDataVector(v, INCLUDE_NOTHING);
    }

    /**
     *	set contents of the combo box to Vector
     */
    public void setDataVector(Vector v, int dfltItem) {
        // cannot have INCLUDE_ALL and single select
        defaultItem = (multi_select == false && dfltItem == SearchableComboBox.INCLUDE_ALL ? INCLUDE_NONE : dfltItem);

        // set data items (filter if the filter is set)
        if (v == null) {
            items = new Vector();
        } else {
            if (filter != null) {
                items = new Vector();
                for (Iterator j = v.iterator(); j.hasNext();) {
                    Object item = j.next();
                    if (filter.isValidItem(item)) {
                        items.add(item);
                    }
                }
            } else {
                items = (Vector) v.clone();
            }
        }

        // setup selection dialog window
        if (dialog != null) {
            dialog.allTm.setTableData(items);
        }

        // add default item
        if ((defaultItem & INCLUDE_ALL) > 0) {
            items.insertElementAt(new SelectAll(), 0);
        }
        if ((defaultItem & INCLUDE_NONE) > 0) {
            items.insertElementAt(new SelectNone(), 0);
        }
        if ((defaultItem & INCLUDE_EMPTY) > 0) {
            items.insertElementAt(new SelectEmpty(), 0);
        }

        // refresh UI
        refresh();
    }

    public void insertItemAt(Object obj, int index) {
        items.insertElementAt(obj, index);
    }

    /**
     * Must be called after insertItemAt method
     * @see #insertItemAt(Object, int)
     */
    public void refresh() {
        DefaultComboBoxModel mdl = new DefaultComboBoxModel(items);
        itemsCoB.setModel(mdl);
    }

    /**
     *	create GUI, set to vector
     */
    public void doInit(Vector v, int dfltItem) {
        try {
            setDataVector(v, dfltItem);
            jbInit();
        } catch (Exception ex) {
            Util.error("SearchableComboBox.doInit()", ex);
            return;
        }
        postInit();
    }

    public void setDisplayStr2Descr(boolean b) {
        displayDescr = b;
        getEditor().setDisplayDescription(b);
    }

    public void setValidate(boolean b) {
        validate = b;
    }

    protected boolean getDisplayDescr() {
        return displayDescr;
    }

    public void hideDetailButton() {
        showDetailButton = false;
        btnDetail.setVisible(false);
    }

    public Vector getDataVector() {
        return items;
    }

    // Useful for positioning on the screen, but not for attaching to a parent:
// brings "parent" upfront obstructing the real parent window if they are different
    public void setDialogParent(Container parent) {
        dialog.setParentContainer(parent);
    }

    public void requestFocus() {
        itemsCoB.requestFocus();
    }

    // change parent properly
    public void resetDialog(JFrame newParent) {
        if (newParent != null) {
            dialog = new SearchableComboDialog(newParent, this, multi_select);
            dialog.allTm.setTableData(items);
        }
    }

    public void resetDialog(JDialog newParent) {
        if (newParent != null) {
            dialog = new SearchableComboDialog(newParent, this, multi_select);
            dialog.allTm.setTableData(items);
        }
    }

    public void setMaximunRowCount(int cnt) {
        itemsCoB.setMaximumRowCount(cnt);
    }

    public void setToolTipText(String text) {
        itemsCoB.setToolTipText(text);
        btnDetail.setToolTipText(text);
    }

    public void setMultipleSelection(boolean multiselect) {
        multi_select = multiselect;
        if (dialog != null && dialog.getParent() != null) {
            if (dialog.getParent() instanceof JFrame)
                resetDialog((JFrame) dialog.getParent());
            else
                resetDialog((JDialog) dialog.getParent());
        }
    }

    public void setFocusBackground(Color fb) {
        focusBG = fb;
    }

    public void setModel(DefaultComboBoxModel mdl) {
        Vector v = new Vector();

        for (int i = 0; i < mdl.getSize(); i++)
            v.add(new SLString(mdl.getElementAt(i).toString()));

        setDataVector(v);
    }

    public void addItems(Object[] arr) {
        Vector v = new Vector();
        for (int i = 0; i < arr.length; i++)
            v.add(new SLString(arr[i].toString()));

        setDataVector(v);
    }

    public void setPopupWidth(int width) {
        itemsCoB.setPopupWidth(width);
    }

    public void setHorizontalScrollBarPolicy(int val) {
        itemsCoB.setHorizontalScrollBarPolicy(val);
    }

    /**
     *	to be overwritten by extending class
     */
    public void setDataVector(String str) {
    }

    /**
     *	to be overwritten by extending class
     */
    public void setDataVector(String str, int i) {
    }


    public void add(JComponent jcp, int w) {
        this.add(jcp, new GridBagConstraints(x_coord, 0, w, 1, 0.0, 0.0
                , GridBagConstraints.EAST,
                GridBagConstraints.NONE,
                new Insets(0, 5, 0, 0), 0, 0));

        x_coord += w;

    }


    public void postInit() {
    }

    public boolean isGroupSelection(String str) {
        if (str == null)
            return false;

        if (SELECT_NONE.equals(str)
                || SELECT_ALL.equals(str)
                || SELECT_EMPTY.equals(str)
        )
            return true;

        return false;
    }


    //--------------------------------------------------------------

    /** @deprecated **/
    public SearchableComboBox(JFrame parent, Vector v, boolean includeDefaultItem) {
        multi_select = true;
        dialog = new SearchableComboDialog(parent, this, multi_select);
        if (includeDefaultItem)
            doInit(v, this.INCLUDE_ALL);
        else
            doInit(v, this.INCLUDE_NOTHING);
    }

    /** @deprecated **/
    public SearchableComboBox(JDialog parent, Vector v, boolean includeDefaultItem) {
        multi_select = true;
        dialog = new SearchableComboDialog(parent, this, multi_select);
        if (includeDefaultItem)
            doInit(v, this.INCLUDE_ALL);
        else
            doInit(v, this.INCLUDE_NOTHING);
    }

    /** @deprecated **/
    public SearchableComboBox(JFrame parent, Vector v, boolean includeDefaultItem, boolean multiSelect) {
        multi_select = multiSelect;
        dialog = new SearchableComboDialog(parent, this, multiSelect);
        if (includeDefaultItem)
            doInit(v, this.INCLUDE_ALL);
        else
            doInit(v, this.INCLUDE_NOTHING);
    }

    /** @deprecated **/
    public SearchableComboBox(JDialog parent, Vector v, boolean includeDefaultItem, boolean multiSelect) {
        multi_select = multiSelect;
        dialog = new SearchableComboDialog(parent, this, multiSelect);
        if (includeDefaultItem)
            doInit(v, this.INCLUDE_ALL);
        else
            doInit(v, this.INCLUDE_NOTHING);
    }

    public void addFocusListenerToTextField(FocusListener l) {
        //  Delegate this to editor component
        itemsCoB.getEditor().getEditorComponent().addFocusListener(l);
    }

    public void removeFocusListenerToTextField(FocusListener l) {
        //  Delegate this to editor component
        itemsCoB.getEditor().getEditorComponent().removeFocusListener(l);
    }

    public Component getFocusableComponent() {
        return itemsCoB.getEditor().getEditorComponent();
    }

    private List getTypedValues() {
        LinkedList result = new LinkedList();
        String[] values = itemsCoB.getEditor().getItem().toString().split(",");
        for (int i = 0; i < values.length; i++) {
            if (select_once && result.contains(values[i])) {
                continue;
            }
            result.add(values[i]);
        }
        return result;
    }

    private Class getContainedObjectsClass() {
        for (Iterator i = items.iterator(); i.hasNext();) {
            SLDataObject item = (SLDataObject) i.next();
            if (!item.isPlaceHolderObject()) {
                return item.getClass();
            }
        }
        return null;
    }


    private class SearchableKeyListener extends KeyAdapter {

        public void keyReleased(KeyEvent e) {
            //ignore "backspace", "left", "delete", "end", "home"
            if (itemsCoB.getEditor().getItem() == null ||
                    e.getKeyCode() == KeyEvent.VK_BACK_SPACE ||
                    e.getKeyCode() == KeyEvent.VK_LEFT ||
                    e.getKeyCode() == KeyEvent.VK_DELETE ||
                    e.getKeyCode() == KeyEvent.VK_END ||
                    e.getKeyCode() == KeyEvent.VK_HOME) {
                return;
            }

            String allText = itemsCoB.getEditor().getItem().toString();
            LinkedList acceptedValues = getAcceptedValues(allText);

            // handles comma
            if (e.getKeyCode() == KeyEvent.VK_COMMA) {
                StringBuffer sb = new StringBuffer(allText.substring(0, allText.length() - 1));
                while (sb.length() > 0 && sb.charAt(sb.length() - 1) == ',') {
                    sb.deleteCharAt(sb.length() - 1);
                }
                if (sb.length() < 1) {
                    itemsCoB.getEditor().setItem("");
                    Util.beep();
                    return;
                }
                String lastWord = sb.toString().substring(sb.toString().lastIndexOf(',') + 1);
                if (multi_select && (!displayDescr && search(lastWord, acceptedValues).listString().equals(lastWord) || displayDescr && search(lastWord, acceptedValues).getDescr().equals(lastWord))) {
                    itemsCoB.getEditor().setItem(sb.toString() + ",");
                    return;
                }
                allText = sb.toString();
                Util.beep();
            }

            String allTextButCurrentWord = (allText.indexOf(",") < 0) ? "" : allText.substring(0, allText.lastIndexOf(",") + 1).trim();
            String currentWord = (allText.indexOf(",") < 0) ? allText : allText.substring(allText.lastIndexOf(",") + 1, allText.length());

            if (currentWord.length() < 1)
                return;

            SLDataObject found = search(currentWord, acceptedValues);

            int initialLength = allTextButCurrentWord.length() + currentWord.length();
            if (found == null) {
                if (!validate) {
                    return;
                }
                StringBuffer sb = new StringBuffer(currentWord.substring(0, currentWord.length() - 1));
                while (found == null && sb.length() > 0) {
                    found = search(sb.toString(), acceptedValues);
                    if (found == null) {
                        sb.deleteCharAt(sb.length() - 1);
                    }
                }
                Util.beep();
                if (found == null) {
                    itemsCoB.getEditor().setItem(allTextButCurrentWord);
                    return;
                }
                initialLength = allTextButCurrentWord.length() + sb.length();
            }

            ActionListener[] actionListeners = itemsCoB.getActionListeners();
            for (int i = 0; i < actionListeners.length; i++) {
                itemsCoB.removeActionListener(actionListeners[i]);
            }
            itemsCoB.setSelectedItem(found);
            String newWord = allTextButCurrentWord + (displayDescr ? found.getDescr() : found.listString());
            itemsCoB.getEditor().setItem(newWord);
            for (int i = 0; i < actionListeners.length; i++) {
                itemsCoB.addActionListener(actionListeners[i]);
                actionListeners[i].actionPerformed(new ActionEvent(this, Event.ACTION_EVENT, "Type"));
            }

            JTextField editorComponent = (JTextField) itemsCoB.getEditor().getEditorComponent();
            editorComponent.getCaret().setDot(initialLength);
            editorComponent.getCaret().moveDot(newWord.length());
        }

        private LinkedList getAcceptedValues(String typed) {
            LinkedList acceptedValues = new LinkedList();
            String[] values = typed.split(",");
            for (int i = 0; i < values.length - 1; i++) {
                acceptedValues.add(values[i]);
            }
            return acceptedValues;
        }
    }

    /**
     * Returns optional filter. Null is returned if no filter is set.
     * @see Filter
     */
    public Filter getFilter() {
        return filter;
    }

    /**
     * Enables filtering the items that are added to the combobox.
     * @see Filter
     */
    public void setFilter(Filter filter) {
        this.filter = filter;

        // remove default items and refresh
        Vector v = new Vector();
        if (items != null) {
            for (Iterator j = items.iterator(); j.hasNext();) {
                Object el = j.next();
                if (!((el instanceof SelectAll) ||
                      (el instanceof SelectNone) ||
                      (el instanceof SelectEmpty))) {
                    v.add(el);
                }
            }
        }
        setDataVector(v, defaultItem);
    }
}
